iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 10
0
Modern Web

RRR撞到不負責之 Laravel + Nuxt.js 踩坑全紀錄系列 第 10

Day 10. FormRequest 管理驗證規則的好幫手

  • 分享至 

  • xImage
  •  

從昨天的最後我們有說分散於各地的驗證規則不好管控,今天會透過以下的主題,整合驗證規則和驗證失敗的處理,同時再工商一個 vaidation 的套件,讓驗證規則可與前端開發共用。

FormRequest

我們可以從 Illuminate\Http\Request 的 api 文件中看到,Request 本身並沒有帶入驗證規則,所以我們才需要在 controller 中自行驗證。好在從 Illuminate\Foundation\Http\FormRequest (後續簡稱 FormRequest) 我們可以看到它具有 validateResolved() 功能,這個 function 是當 FormRequest 產生實例後會去執行、驗證本身是否合法:

trait ValidatesWhenResolvedTrait
{
    /**
     * Validate the class instance.
     *
     * @return void
     */
    public function validateResolved()
    {
        $this->prepareForValidation();

        if (! $this->passesAuthorization()) {
            $this->failedAuthorization();
        }

        $instance = $this->getValidatorInstance();

        if ($instance->fails()) {
            $this->failedValidation($instance);
        }
    }

下面是將驗證的規則跟客製的錯誤訊息寫至 FormRequest 的作法:

  1. 在 cmd 中執行以下指令產生位置在 App\Http\Requests 的 FormRequest。
php artisan make:request <Request 名稱>
  1. 預設的 FormRequest 主要提供我們設定「使用者授權」和 「request 驗證」兩個部份的設定,我們主要討論 validation 的部分,其他大家可以更進一步參考官網文件說明
class EditPostFormRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        // 預設為 false,如果不需要授權驗證,要改為 true
        return false;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            //
        ];
    }
}
  1. 接著我們將昨天的驗證規則寫入 rules() 回傳的 array 當中:
    // ...
    public function rules()
    {
        return [
            "userId" => "required|integer|exists:users,id",
            "postId" => "required|integer|exists:posts,id",
            "title" => "nullable|string",
            "content" => "nullable|string"
        ];
    }
  1. 客製錯誤訊息的部分是透過 override FormRequest 的 messages() 方法:
    // ...
    public function messages()
    {
        return [
            "userId.required" => "使用者 ID 為避填資料",
            "userId.exists" => '使用者 ID 必須存在於資料庫中',
            "postId.integer" => "文章 ID 必須為數值",
            "postId.exists" => "文章 ID 不存在"
        ];
    }
  1. 我們從 validateResolved() 方法中可以看到,當驗證沒有通過,會執行 failedValidation() 方法,同時再往下追,可以在 FormRequest 中可以看到,除了丟出 ValidationException 之外,另外還會進行跳轉。
    protected function failedValidation(Validator $validator)
    {
        throw (new ValidationException($validator))
                    ->errorBag($this->errorBag)
                    ->redirectTo($this->getRedirectUrl());
    }

所以假若我們希望在驗證失敗的時候做一些處理,可以 override failedValidation()。兩個常用情境介紹:

  • 不做任何動作,由 controller 處理: 需要將 getValidatorInstance() 改為 public,讓 controller 可以取到 validator 實例並進一步得到錯誤訊息。
class EditPostFormRequest extends FormRequest
{
    // ...
    protected function failedValidation(Validator $validator)
    {
    }

    // override getValidatorInstance,將 protected 改為 publick
    public function getValidatorInstance()
    {
        return parent::getValidatorInstance();
    }
}

class PostController extends Controller
{
    public function edit(EditPostFormRequest $request)
    {
        $validator = $request->getValidatorInstance();
        if ($validator->fails()) {
            $errorMessage = $validator->getMessageBag()->getMessages();
            // ...
        }
    }
}
  • 直接回傳「特定格式」的 response 並帶上驗證錯誤資訊。(如果沒有特殊需求,這個作法相對方便)。
class EditPostFormRequest extends FormRequest
{
    // ...
    protected function failedValidation(Validator $validator)
    {
        // 取得錯誤資訊
        $responseData = $validator->errors();
        // 產生 JSON 格式的 response,(422 是 Laravel 預設的錯誤 http status,可自行更換) 
        $response = response()->json($responseData, 422);
        // 丟出 exception
        throw new HttpResponseException($response);
    }
  1. 至此,我們將驗證的處理交由 FormRequest 處理,當 controller function 被執行時,藉由 DI 注入產生 FormRequest 實例的時候就會自動的進行驗證。如此一來,我們也可以比較好重複使用這些 FormRequest,同時 controller 也變得更為簡潔:
    function edit(PostService $postService, EditPostFormRequest $request)
    {
        $updateData = $request->only(['title', 'content']);
        $userId = $request['userId'];
        $postId = $request['postId'];
        
        try {
            $updatedPost = $postService->updatePost($userId, $postId, $updateData);
            $updatedPost = $postService->modelToAPIResource(updatedPost);
            return response()->json([
                'success' => true,
                'message' => null,
                'data' => $updatedPost
            ]);
        } catch (\Exception $exception) {
            $exMessage = $exception.getMessage();
            $exCode = $exception.getCode();
            return response()->json([
                'success' => false,
                'message' => "catch exception:{$exMessage}",
                'code' => $exCode,
            ], 500);
        }
    }

lara-validator

進一步的,如果希望這些驗證規則也能提供前端開發使用,不妨參考小弟之前寫的小工具 semantic-lab/lara-validator (packagistGitLab)。

  1. 首先在 cmd 執行下面指令安裝套件。安裝後會增加下列兩項資料:
  • config\validators: 接下來要存放各種驗證規則的地方
  • app\Http\Responses: 簡易的 response 類別
composer require semantic-lab/lara-validator --dev
  1. 接著在 config\validators 撰寫若干 json 設定檔,主要設定欄位如下:
  • subNamespace
  • validators
  • failResponse
  1. 最後在 cmd 中執行下列指令,會讀取 Config\validators 底下所有 json 設定檔,產出 FormRequest。
php artisan validator:make

subNamespace

subNamespace 代表之後產生的 FormRequest 會在 app\Http\Request 下的哪一個資料夾。若沒有設定,預設一樣會在 app\Http\Request 底下,例如: "subNamespace": "Post",則這份檔案所有的 FormRequest 都會建立在 app\Http\Request\Post 底下。

validators

這裡主要設定各個 FromRequest 的驗證規則和客製訊息。

  • validators 底下的每一個 key 都會是之後產生的 FormRequest 類別名稱
  • 驗證方法寫在 body 裡面。
{
    "validators": {
        "EditPostRequest": {
            "body": {
                "userId": "required|integer|exists:users,id",
                "postId": "required|integer|exists:posts,id",
                "title": "nullable|string",
                "content": "nullable|string"
            }
        },
        "CreatePostRequest": {
            // ...
        },
        // ...
    }
}

若要客製訊息,可以改在保留字 rules 底下設定,如果驗證方法的訊息設為 null,產生之後會使用 Laravel 預設錯誤訊息。

{
    "validators": {
        "EditPostRequest": {
            "body": {
                "userId": {
                    "rules": {
                        "required": "使用者 ID 為必填資料",
                        "integer": null,
                        "exists:users,id": "使用者 ID 必須存在於資料庫中"
                    }
                },
                "postId": {
                    "rules": {
                        "required": null,
                        "integer": "文章 ID 必須為數值",
                        "exists:posts,id": "文章 ID 不存在"
                    }
                },
            }
        },
    }
}

巢狀資料的設定就是不斷以 sub-object 往下定義即可,只是需要留意要驗證的資料欄位名稱不可為 rules 保留字。

{
    "validators": {
        "EditPostRequest": {
            "body": {
                "user": {
                    "id": "required|integer|exists:users,id"
                },
                "post": {
                    "id": "required|integer|exists:posts,id",
                    "title": "nullable|string",
                    "content": "nullable|string"
                }
            }
        }
    }
}

陣列資料的設定,稍微特殊一些,我們會用相同的驗證規則,驗證每個陣列裡的每一個元素。

{
    "validators": {
        "EditMultiPostsRequest": {
            "body": {
                "user": {
                    "id": "required|integer|exists:users,id"
                },
                "posts": [{
                    "id": "required|integer|exists:posts,id",
                    "title": "nullable|string",
                    "content": "nullable|string"
                }]
            }
        }
    }
}

failResponse

failResponse 是用來設定檔案內所有 validators 驗證錯誤的處理方法。

類型 說明
default Laravel 預設跳轉至首頁
ignore 不做任何動作,依然進入到 controller function (action) 中
exception 丟出 \Illuminate\Support\MessageBag 作為 response
response 回傳設定的 IResponse 資料
{
    // ...
    "failResponse": {
        // default, ignore, exception 或 response
        "type": "response",
        // exception 或 response 可以指定 response 的 HTTP staus (預設為 422)
        "httpStatus": 200,
        // response 所需要設定的類別,且此類別必須實作 IResponse, 
        "class": "APIResponse"
    }
}

上面的範例是採用「response (回傳設定的 IResponse 資料)」的方式,其中,class 的部分會對應到 app\Http\ResponseAPIResponse (不是預設,需實作)。


有了 FormRequest 之後,我們可以再將驗證資料的工作,從 controller 拆出來改由 FormRequest 處理,除了 controller 更簡潔之外,進一步的我們還有機會可以重複利用 FormRequest。

Packagist 有不少驗證套件,semantic-lab/lara-validator 的部分大家可以參考看看,如果套件有問題或是有需要改進也歡迎留言或是發 issue。除了 PHP 的部分,小弟也有寫相對應的 JavaScript 套件,這部分就到 Nuxt.js 的部分再介紹了!

總之至目前為止我們已經可以完整的寫 API 了。明天我們會說明 Laravel Routing 的設定,然後就可以跑看看我們的 API 到底有沒有成功啦!


上一篇
Day 09. Request 驗證可以再簡單一點 (Validation)
下一篇
Day 11. 第一個 Laravel API 終於生出來惹 (´;ω;`)
系列文
RRR撞到不負責之 Laravel + Nuxt.js 踩坑全紀錄31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言